Débloquez la puissance de `createPortal` de React pour une gestion avancée de l'interface utilisateur, les fenêtres modales, les info-bulles et le dépassement des limitations CSS z-index.
Maîtriser les superpositions d'interface utilisateur : un examen approfondi de la fonction `createPortal` de React
Dans le développement web moderne, la création d'interfaces utilisateur transparentes et intuitives est primordiale. Souvent, cela implique l'affichage d'éléments qui doivent s'extraire de la hiérarchie DOM de leur composant parent. Pensez aux boîtes de dialogue modales, aux bannières de notification, aux info-bulles ou même aux menus contextuels complexes. Ces éléments d'interface utilisateur nécessitent fréquemment une manipulation spéciale pour garantir qu'ils sont rendus correctement, en se superposant au-dessus des autres contenus sans interférence des contextes d'empilement z-index CSS.
React, dans son évolution continue, fournit une solution puissante pour ce défi précis : la fonction createPortal. Cette fonctionnalité, disponible via react-dom, vous permet de rendre des composants enfants dans un nœud DOM qui existe en dehors de la hiérarchie normale des composants React. Cet article de blog servira de guide complet pour comprendre et utiliser efficacement createPortal, en explorant ses concepts fondamentaux, ses applications pratiques et ses meilleures pratiques pour un public de développement mondial.
Qu'est-ce que `createPortal` et pourquoi l'utiliser ?
À la base, React.createPortal(child, container) est une fonction qui rend un composant React (le child) dans un nœud DOM différent (le container) de celui qui est un parent du composant React dans l'arborescence React.
Décomposons les paramètres :
child: Il s'agit de l'élément React, de la chaîne ou du fragment que vous souhaitez rendre. C'est essentiellement ce que vous renverriez normalement à partir de la méthoderenderd'un composant.container: Il s'agit d'un élément DOM qui existe dans votre document. C'est la cible où lechildsera ajouté.
Le problème : hiérarchie DOM et contextes d'empilement CSS
Considérez un scénario courant : une boîte de dialogue modale. Les modales sont généralement destinées à être affichées au-dessus de tout autre contenu de la page. Si vous rendez un composant modal directement dans un autre composant qui a un style overflow : hidden restrictif ou une valeur z-index spécifique, la modale peut être coupée ou incorrectement superposée. Cela est dû à la nature hiérarchique du DOM et aux règles de contexte d'empilement z-index du CSS.
Une valeur z-index sur un élément n'affecte son ordre d'empilement que par rapport à ses frères et sœurs dans le même contexte d'empilement. Si un élément ancêtre établit un nouveau contexte d'empilement (par exemple, en ayant une position autre que static et un z-index), les enfants rendus dans cet ancêtre seront confinés à ce contexte. Cela peut entraîner des problèmes de mise en page frustrants où votre superposition prévue est enfouie sous d'autres éléments.
La solution : `createPortal` à la rescousse
createPortal résout élégamment ce problème en rompant la connexion visuelle entre la position du composant dans l'arborescence React et sa position dans l'arborescence DOM. Vous pouvez rendre un composant dans un portail, et il sera ajouté directement à un nœud DOM qui est un frère ou un enfant du body, contournant ainsi efficacement les contextes d'empilement d'ancêtres problématiques.
Même si le portail rend son enfant dans un nœud DOM différent, il se comporte toujours comme un composant React normal dans votre arborescence React. Cela signifie que la propagation des événements fonctionne comme prévu : si un gestionnaire d'événements est attaché à un composant rendu par un portail, l'événement remontera toujours dans la hiérarchie des composants React, pas seulement dans la hiérarchie DOM.
Cas d'utilisation clés de `createPortal`
La polyvalence de createPortal en fait un outil indispensable pour divers modèles d'interface utilisateur :
1. Fenêtres modales et boîtes de dialogue
C'est peut-être le cas d'utilisation le plus courant et le plus convaincant. Les modales sont conçues pour interrompre le flux de travail de l'utilisateur et exiger son attention. Leur rendu directement dans un composant peut entraîner des problèmes de contexte d'empilement.
Exemple de scénario : Imaginez une application de commerce électronique où les utilisateurs doivent confirmer une commande. La modale de confirmation doit apparaître au-dessus de tout le reste sur la page.
Idée de mise en œuvre :
- Créez un élément DOM dédié dans votre fichier
public/index.html(ou créez-en un dynamiquement). Une pratique courante consiste à avoir un<div id="modal-root"></div>, souvent placé à la fin de la balise<body>. - Dans votre application React, obtenez une référence à ce nœud DOM.
- Lorsque votre composant modal est déclenché, utilisez
ReactDOM.createPortalpour rendre le contenu de la modale dans le nœud DOMmodal-root.
Extrait de code (conceptuel) :
// App.js
import React from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = React.useState(false);
return (
<div>
<h1>Bienvenue dans notre magasin mondial !</h1>
<button onClick={() => setIsModalOpen(true)}>Afficher la confirmation</button>
{isModalOpen && (
<Modal onClose={() => setIsModalOpen(false)}>
<h2>Confirmer votre achat</h2>
<p>Êtes-vous sûr de vouloir continuer ?</p>
</Modal>
)}
</div>
);
}
export default App;
// Modal.js
import React from 'react';
import ReactDOM from 'react-dom';
const modalRoot = document.getElementById('modal-root');
function Modal({ children, onClose }) {
// Create a DOM element for the modal content to live in
const element = document.createElement('div');
React.useEffect(() => {
// Append the element to the modal root when the component mounts
modalRoot.appendChild(element);
// Clean up by removing the element when the component unmounts
return () => {
modalRoot.removeChild(element);
};
}, [element]);
return ReactDOM.createPortal(
<div className="modal-backdrop">
<div className="modal-content">
{children}
<button onClick={onClose}>Fermer</button>
</div>
</div>,
element // Render into the element we created
);
}
export default Modal;
Cette approche garantit que la modale est un enfant direct de modal-root, qui est généralement ajouté au body, contournant ainsi tout contexte d'empilement intermédiaire.
2. Info-bulles et popovers
Les info-bulles et les popovers sont de petits éléments d'interface utilisateur qui apparaissent lorsqu'un utilisateur interagit avec un autre élément (par exemple, passe la souris sur un bouton ou clique sur une icône). Ils doivent également apparaître au-dessus des autres contenus, surtout si l'élément déclencheur est imbriqué profondément dans une mise en page complexe.
Exemple de scénario : Dans une plateforme de collaboration internationale, un utilisateur passe la souris sur l'avatar d'un membre de l'équipe pour voir ses coordonnées et son statut de disponibilité. L'info-bulle doit être visible quel que soit le style du conteneur parent de l'avatar.
Idée de mise en œuvre : Semblable aux modales, vous pouvez créer un portail pour rendre les info-bulles. Un modèle courant consiste à attacher l'info-bulle à une racine de portail commune, ou même directement au body si vous n'avez pas de conteneur de portail spécifique.
Extrait de code (conceptuel) :
// Tooltip.js
import React from 'react';
import ReactDOM from 'react-dom';
function Tooltip({ children, targetElement }) {
if (!targetElement) return null;
// Render the tooltip content directly into the body
return ReactDOM.createPortal(
<div className="tooltip">
{children}
</div>,
document.body
);
}
// Parent Component that triggers the tooltip
function InfoButton({ info }) {
const [targetRef, setTargetRef] = React.useState(null);
const [showTooltip, setShowTooltip] = React.useState(false);
return (
<div
ref={setTargetRef} // Get the DOM element of this div
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
style={{ position: 'relative', display: 'inline-block' }}
>
<i>?</i> {/* Information icon */}
{showTooltip && <Tooltip targetElement={targetElement}>{info}</Tooltip>}
</div>
);
}
3. Menus déroulants et zones de sélection
Les menus déroulants et les zones de sélection personnalisés peuvent également bénéficier des portails. Lorsqu'un menu déroulant est ouvert, il doit souvent s'étendre au-delà des limites de son conteneur parent, surtout si ce conteneur a des propriétés telles que overflow : hidden.
Exemple de scénario : Le tableau de bord interne d'une entreprise multinationale comprend un menu déroulant de sélection personnalisé permettant de choisir un projet dans une longue liste. La liste déroulante ne doit pas être limitée par la largeur ou la hauteur du widget de tableau de bord dans lequel elle réside.
Idée de mise en œuvre : Rendez les options du menu déroulant dans un portail attaché au body ou à une racine de portail dédiée.
4. Systèmes de notification
Les systèmes de notification globaux (messages toast, alertes) sont un autre excellent candidat pour createPortal. Ces messages apparaissent généralement dans une position fixe, souvent en haut ou en bas de la fenêtre d'affichage, quelle que soit la position de défilement actuelle ou la mise en page du composant parent.
Exemple de scénario : Un site de réservation de voyages affiche des messages de confirmation pour les réservations réussies ou des messages d'erreur pour les paiements échoués. Ces notifications doivent apparaître de manière cohérente sur l'écran de l'utilisateur.
Idée de mise en œuvre : Un conteneur de notification dédié (par exemple, <div id="notifications-root"></div>) peut être utilisé avec createPortal.
Comment implémenter `createPortal` dans React
L'implémentation de createPortal implique quelques étapes clés :
Étape 1 : Identifier ou créer un nœud DOM cible
Vous avez besoin d'un élément DOM en dehors de la racine React standard pour servir de conteneur pour le contenu de votre portail. La pratique la plus courante consiste à définir cela dans votre fichier HTML principal (par exemple, public/index.html).
<!-- public/index.html -->
<body>
<noscript>Vous devez activer JavaScript pour exécuter cette application.</noscript>
<div id="root"></div>
<div id="modal-root"></div> <!-- For modals -->
<div id="tooltip-root"></div> <!-- Optionally for tooltips -->
</body>
Alternativement, vous pouvez créer dynamiquement un élément DOM dans le cycle de vie de votre application à l'aide de JavaScript, comme indiqué dans l'exemple Modal ci-dessus, puis l'ajouter au DOM. Cependant, la prédéfinition en HTML est généralement plus propre pour les racines de portail persistantes.
Étape 2 : Obtenir une référence au nœud DOM cible
Dans votre composant React, vous devrez accéder à ce nœud DOM. Vous pouvez le faire en utilisant document.getElementById() ou document.querySelector().
// Somewhere in your component or utility file
const modalRootElement = document.getElementById('modal-root');
const tooltipRootElement = document.getElementById('tooltip-root');
// It's crucial to ensure these elements exist before attempting to use them.
// You might want to add checks or handle cases where they are not found.
Étape 3 : Utiliser `ReactDOM.createPortal`
Importez ReactDOM et utilisez la fonction createPortal, en passant le JSX de votre composant comme premier argument et le nœud DOM cible comme second.
Exemple : Rendu d'un message simple dans un portail
// MessagePortal.js
import React from 'react';
import ReactDOM from 'react-dom';
function MessagePortal({ message }) {
const portalContainer = document.getElementById('modal-root'); // Assuming you're using modal-root for this example
if (!portalContainer) {
console.error('Portal container "modal-root" not found!');
return null;
}
return ReactDOM.createPortal(
<div style={{ position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)', backgroundColor: 'rgba(0,0,0,0.7)', color: 'white', padding: '10px', borderRadius: '5px' }}>
{message}
</div>,
portalContainer
);
}
export default MessagePortal;
// In another component...
function Dashboard() {
return (
<div>
<h1>Aperçu du tableau de bord</h1>
<MessagePortal message="Données synchronisées avec succès !" />
</div>
);
}
Gestion de l'état et des événements avec les portails
L'un des avantages les plus importants de createPortal est qu'il ne rompt pas le système de gestion des événements de React. Les événements des éléments rendus à l'intérieur d'un portail remonteront toujours dans l'arborescence des composants React, pas seulement dans l'arborescence DOM.
Exemple de scénario : Une boîte de dialogue modale peut contenir un formulaire. Lorsqu'un utilisateur clique sur un bouton à l'intérieur de la modale, l'événement de clic doit être géré par un écouteur d'événements dans le composant parent qui contrôle la visibilité de la modale, et non être piégé dans la hiérarchie DOM de la modale elle-même.
Exemple illustratif :
// ModalWithEventHandling.js
import React from 'react';
import ReactDOM from 'react-dom';
const modalRoot = document.getElementById('modal-root');
function ModalWithEventHandling({ children, onClose }) {
const modalContentRef = React.useRef(null);
// Using useEffect to create and clean up the DOM element
const [wrapperElement] = React.useState(() => document.createElement('div'));
React.useEffect(() => {
modalRoot.appendChild(wrapperElement);
return () => {
modalRoot.removeChild(wrapperElement);
};
}, [wrapperElement]);
// Handle clicks outside the modal content to close it
const handleOutsideClick = (event) => {
if (modalContentRef.current && !modalContentRef.current.contains(event.target)) {
onClose();
}
};
return ReactDOM.createPortal(
<div className="modal-backdrop" onClick={handleOutsideClick}>
<div className="modal-content" ref={modalContentRef}>
{children}
<button onClick={onClose}>Fermer la modale</button>
</div>
</div>,
wrapperElement
);
}
// App.js (using the modal)
function App() {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<h1>Contenu de l'application</h1>
<button onClick={() => setShowModal(true)}>Ouvrir la modale</button>
{showModal && (
<ModalWithEventHandling onClose={() => setShowModal(false)}>
<h2>Informations importantes</h2>
<p>Ceci est le contenu à l'intérieur de la modale.</p>
<button onClick={() => alert('Bouton à l'intérieur de la modale cliqué !')}>
Bouton d'action
</button>
</ModalWithEventHandling>
)}
</div>
);
}
Dans cet exemple, cliquer sur le bouton Fermer la modale appelle correctement la prop onClose transmise par le composant App parent. De même, si vous aviez un écouteur d'événements pour les clics sur le modal-backdrop, il déclencherait correctement la fonction handleOutsideClick, même si la modale est rendue dans un sous-arbre DOM distinct.
Modèles avancés et considérations
Portails dynamiques
Vous pouvez créer et supprimer des conteneurs de portail dynamiquement en fonction des besoins de votre application, bien que le maintien de racines de portail persistantes et prédéfinies soit souvent plus simple.
Portails et rendu côté serveur (SSR)
Lorsque vous travaillez avec le rendu côté serveur (SSR), vous devez être attentif à la façon dont les portails interagissent avec le HTML initial. Étant donné que les portails rendent dans des nœuds DOM qui peuvent ne pas exister sur le serveur, vous devez souvent rendre le contenu du portail de manière conditionnelle ou vous assurer que les nœuds DOM cibles sont présents dans la sortie SSR.
Un modèle courant consiste à utiliser un hook comme useIsomorphicLayoutEffect (ou un hook personnalisé qui privilégie useLayoutEffect sur le client et revient à useEffect sur le serveur) pour garantir que la manipulation du DOM ne se produit que sur le client.
// usePortal.js (a common utility hook pattern)
import React, { useRef, useEffect } from 'react';
function usePortal(id) {
const modalRootRef = useRef(null);
useEffect(() => {
let currentModalRoot = document.getElementById(id);
if (!currentModalRoot) {
currentModalRoot = document.createElement('div');
currentModalRoot.setAttribute('id', id);
document.body.appendChild(currentModalRoot);
}
modalRootRef.current = currentModalRoot;
// Cleanup function to remove the created element if it was created by this hook
return () => {
// Be cautious with cleanup; only remove if it was actually created here
// A more robust approach might involve tracking element creation.
};
}, [id]);
return modalRootRef.current;
}
export default usePortal;
// Modal.js (using the hook)
import React from 'react';
import ReactDOM from 'react-dom';
import usePortal from './usePortal';
function Modal({ children, onClose }) {
const portalTarget = usePortal('modal-root'); // Use our hook
if (!portalTarget) return null;
return ReactDOM.createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}> {/* Prevent closing by clicking inside */}
{children}
</div>
</div>,
portalTarget
);
}
Pour SSR, vous devez généralement vous assurer que le div modal-root existe dans votre HTML rendu côté serveur. L'application React sur le client s'y attachera ensuite.
Styling Portals
Le style des éléments dans un portail nécessite une attention particulière. Étant donné qu'ils se trouvent souvent en dehors du contexte de style du parent direct, vous pouvez appliquer des styles globaux ou utiliser des modules CSS/composants stylisés pour gérer efficacement l'apparence du contenu du portail.
Pour les superpositions comme les modales, vous aurez souvent besoin de styles qui :
- Fixez l'élément à la fenêtre d'affichage (
position : fixed). - Couvrez toute la fenêtre d'affichage (
top : 0 ; left : 0 ; width : 100 % ; height : 100 % ;). - Utilisez une valeur
z-indexélevée pour vous assurer qu'elle apparaît au-dessus de tout le reste. - Incluez un arrière-plan semi-transparent pour la toile de fond.
Accessibilité
Lors de la mise en œuvre de modales ou d'autres superpositions, l'accessibilité est cruciale. Assurez-vous de gérer correctement la mise au point :
- Lorsqu'une modale s'ouvre, piègez la mise au point à l'intérieur de la modale. Les utilisateurs ne doivent pas pouvoir sortir de la modale avec la touche Tab.
- Lorsque la modale se ferme, renvoyez la mise au point à l'élément qui l'a déclenchée.
- Utilisez les attributs ARIA (par exemple,
role="dialog",aria-modal="true",aria-labelledby,aria-describedby) pour informer les technologies d'assistance de la nature de la modale.
Les bibliothèques telles que Reach UI ou Material-UI fournissent souvent des composants modaux accessibles qui gèrent ces problèmes pour vous.
Pièges potentiels et comment les éviter
Oublier le nœud DOM cible
L'erreur la plus courante est d'oublier de créer le nœud DOM cible dans votre HTML ou de ne pas le référencer correctement dans votre JavaScript. Assurez-vous toujours que votre conteneur de portail existe avant d'essayer d'y effectuer un rendu.
Propagation des événements vs. Propagation DOM
Bien que les événements React se propagent correctement via les portails, les événements DOM natifs ne le font pas. Si vous attachez des écouteurs d'événements DOM natifs directement aux éléments dans un portail, ils ne remonteront que dans l'arborescence DOM, pas dans l'arborescence des composants React. Tenez-vous-en au système d'événements synthétiques de React dans la mesure du possible.
Chevauchement des portails
Si vous avez plusieurs types de superpositions (modales, info-bulles, notifications) qui rendent toutes vers le corps ou une racine commune, la gestion de leur ordre d'empilement peut devenir complexe. L'attribution de valeurs z-index spécifiques ou l'utilisation d'un système de gestion de portail peuvent aider.
Considérations relatives aux performances
Bien que createPortal lui-même soit efficace, le rendu de composants complexes dans les portails peut toujours avoir un impact sur les performances. Assurez-vous que le contenu de votre portail est optimisé et évitez les rendus inutiles.
Alternatives à `createPortal`
Bien que createPortal soit la façon idiomatique de React de gérer ces scénarios, il convient de noter d'autres approches que vous pourriez rencontrer ou envisager :
- Manipulation directe du DOM : Vous pouvez créer et ajouter manuellement des éléments DOM à l'aide de
document.createElementetappendChild, mais cela contourne le rendu déclaratif et la gestion de l'état de React, ce qui le rend moins maintenable. - Composants d'ordre supérieur (HOC) ou Render Props : Ces modèles peuvent abstraire la logique du rendu du portail, mais
createPortallui-même est le mécanisme sous-jacent. - Bibliothèques de composants : De nombreuses bibliothèques de composants d'interface utilisateur (par exemple, Material-UI, Ant Design, Chakra UI) fournissent des composants modaux, info-bulles et déroulants prédéfinis qui abstraient l'utilisation de
createPortal, offrant une expérience de développement plus pratique. Cependant, la compréhension decreatePortalest essentielle pour personnaliser ces composants ou créer les vôtres.
Conclusion
React.createPortal est une fonctionnalité puissante et essentielle pour créer des interfaces utilisateur sophistiquées dans React. En vous permettant de rendre des composants dans des nœuds DOM en dehors de leur hiérarchie d'arborescence React, il résout efficacement les problèmes courants liés au z-index CSS, aux contextes d'empilement et au dépassement d'éléments.
Que vous créiez des boîtes de dialogue modales complexes pour la confirmation de l'utilisateur, des info-bulles subtiles pour des informations contextuelles ou des bannières de notification visibles globalement, createPortal fournit la flexibilité et le contrôle nécessaires. N'oubliez pas de gérer vos nœuds DOM de portail, de gérer correctement les événements et de donner la priorité à l'accessibilité et aux performances pour une application véritablement robuste et conviviale, adaptée à un public mondial avec divers horizons et besoins techniques.
Maîtriser createPortal élèvera sans aucun doute vos compétences en développement React, vous permettant de créer des interfaces utilisateur plus soignées et professionnelles qui se démarquent dans le paysage de plus en plus complexe des applications web modernes.